window.addEventListener('load', () => {
  const light = {
    x: 0,
    y: 7,
    z: -5
  };

  const canvas = document.getElementById('canvas');
  const [offsetWidth, offsetHeight] = [canvas.width, canvas.height];
  const gl = canvas.getContext('webgl');
  const program = createProgramByMe(gl, vertex_shader_source, fragment_shader_source);
  const shadowProgram = createProgramByMe(gl, shadow_vertex_shader_source, shadow_fragment_shader_source);

  const [shadowObj, normalObj, cubeObj, panelObj] = [{}, {}, {}, {}];

  shadowObj.a_Position = gl.getAttribLocation(shadowProgram, 'a_Position');
  shadowObj.u_MvpMatrix = gl.getUniformLocation(shadowProgram, 'u_MvpMatrix');

  normalObj.a_Position = gl.getAttribLocation(program, 'a_Position');
  normalObj.a_Color = gl.getAttribLocation(program, 'a_Color');
  normalObj.u_MvpMatrix = gl.getUniformLocation(program, 'u_MvpMatrix');
  normalObj.u_MvpMatrixFromLight = gl.getUniformLocation(program, 'u_MvpMatrixFromLight');
  normalObj.u_ShadowMap = gl.getUniformLocation(program, 'u_ShadowMap');

  cubeObj.vertexBuffer = initBuffer(gl, gl.ARRAY_BUFFER, cubeData.vertices);
  cubeObj.colorBuffer = initBuffer(gl, gl.ARRAY_BUFFER, cubeData.colors);
  cubeObj.indicesBuffer = initBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, cubeData.indices);

  panelObj.vertexBuffer = initBuffer(gl, gl.ARRAY_BUFFER, panelData.vertices);
  panelObj.colorBuffer = initBuffer(gl, gl.ARRAY_BUFFER, panelData.colors);
  panelObj.indicesBuffer = initBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, panelData.indices);

  let fbo = initFramebuffer(gl, offsetWidth, offsetHeight);

  let viewProjMatrix = new Matrix4();

  let viewProjMatrixFromLight = new Matrix4(); // Prepare a view projection matrix for generating a shadow map
  viewProjMatrixFromLight.setPerspective(70.0, canvas.width / canvas.height, 1.0, 100.0);
  viewProjMatrixFromLight.lookAt(light.x, light.y, 5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

  viewProjMatrix.setPerspective(45, canvas.width / canvas.clientHeight, 1.0, 100.0);
  viewProjMatrix.lookAt(0.0, 7.0, 50.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

  let currentAngle = 0;
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.enable(gl.DEPTH_TEST);
  let newViewProjMatrix = new Matrix4();
  let matrix = new Matrix4();
  const draw = () => {
    currentAngle += 1 % 360;
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    matrix.setRotate(currentAngle, 1.0, 0.0, 0.0);
    matrix.rotate(currentAngle, 0.0, 1.0, 0.0);
    matrix.rotate(currentAngle, 0.0, 0.0, 1.0);

    gl.useProgram(shadowProgram);
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
    gl.viewport(0, 0, offsetWidth, offsetHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeObj.vertexBuffer);
    gl.vertexAttribPointer(shadowObj.a_Position, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(cubeObj.a_Position);

    gl.uniformMatrix4fv(shadowObj.u_MvpMatrix, false, newViewProjMatrix.set(viewProjMatrixFromLight).multiply(matrix).elements);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeObj.indicesBuffer);
    gl.drawElements(gl.TRIANGLES, cubeData.indices.length, gl.UNSIGNED_BYTE, 0);

    // panel
    gl.bindBuffer(gl.ARRAY_BUFFER, panelObj.vertexBuffer);
    gl.vertexAttribPointer(shadowObj.a_Position, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(shadowObj.a_Position);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, panelObj.indicesBuffer);

    gl.uniformMatrix4fv(shadowObj.u_MvpMatrix, false, viewProjMatrixFromLight.elements);
    gl.drawElements(gl.TRIANGLES, panelData.indices.length, gl.UNSIGNED_BYTE, 0);

    // -----------  normal
    gl.useProgram(program);
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.uniformMatrix4fv(normalObj.u_MvpMatrixFromLight, false, viewProjMatrixFromLight.elements);

    // cube
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeObj.vertexBuffer);
    gl.vertexAttribPointer(normalObj.a_Position, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(cubeObj.a_Position);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeObj.colorBuffer);
    gl.vertexAttribPointer(normalObj.a_Color, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(normalObj.a_Color);

    gl.uniformMatrix4fv(normalObj.u_MvpMatrix, false, newViewProjMatrix.set(viewProjMatrix).multiply(matrix).elements);

    gl.uniform1i(normalObj.u_ShadowMap, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeObj.indicesBuffer);
    gl.drawElements(gl.TRIANGLES, cubeData.indices.length, gl.UNSIGNED_BYTE, 0);

    // panel
    gl.bindBuffer(gl.ARRAY_BUFFER, panelObj.vertexBuffer);
    gl.vertexAttribPointer(normalObj.a_Position, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(normalObj.a_Position);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, panelObj.indicesBuffer);

    gl.bindBuffer(gl.ARRAY_BUFFER, panelObj.colorBuffer);
    gl.vertexAttribPointer(normalObj.a_Color, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(normalObj.colors);
    gl.uniformMatrix4fv(normalObj.u_MvpMatrix, false, viewProjMatrix.elements);

    gl.drawElements(gl.TRIANGLES, panelData.indices.length, gl.UNSIGNED_BYTE, 0);
    requestAnimationFrame(draw);
  };

  draw();
});

const createProgramByMe = (gl, v_shader_source, f_shader_source) => {
  const v_shader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(v_shader, v_shader_source);
  gl.compileShader(v_shader);

  const f_shader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(f_shader, f_shader_source);
  gl.compileShader(f_shader);

  let compiled = gl.getShaderParameter(f_shader, gl.COMPILE_STATUS);
  if (!compiled) {
    let error = gl.getShaderInfoLog(f_shader);
    console.log('Failed to compile shader: ' + error);
    gl.deleteShader(f_shader);
    return null;
  }

  const program = gl.createProgram();
  gl.attachShader(program, v_shader);
  gl.attachShader(program, f_shader);
  gl.linkProgram(program);
  let linked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!linked) {
    console.log(gl.getProgramInfoLog(program));
  }

  return program;
};

const initBuffer = (gl, target = gl.ARRAY_BUFFER, data = []) => {
  let buffer = gl.createBuffer();
  gl.bindBuffer(target, buffer);
  gl.bufferData(target, data, gl.STATIC_DRAW);
  gl.bindBuffer(target, null);
  return buffer;
};

const initFramebuffer = (gl, offsetWidth, offsetHeight) => {
  let texture = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, offsetWidth, offsetHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  let depthBuffer = gl.createRenderbuffer();
  gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
  gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, offsetWidth, offsetHeight);

  let framebuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);

  framebuffer.texture = texture;

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  return framebuffer;
};

const panelData = {
  vertices: new Float32Array([
    4.0, -3.7, 2.5, -4.0, -3.7, 2.5, -4.0, -3.7, -2.5, 4.0, -3.7, -2.5 // v0-v1-v2-v3
  ]),
  colors: new Float32Array([
    1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
  ]),
  indices: new Uint8Array([0, 1, 2, 0, 2, 3])
};

const cubeData = {
  vertices: new Float32Array([ // Vertex coordinates
    1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // v0-v1-v2-v3 front
    1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, // v0-v3-v4-v5 right
    1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 up
    -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, // v1-v6-v7-v2 left
    -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, // v7-v4-v3-v2 down
    1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0 // v4-v7-v6-v5 back
  ]),
  // Indices of the vertices
  indices: new Uint8Array([
    0, 1, 2, 0, 2, 3, // front
    4, 5, 6, 4, 6, 7, // right
    8, 9, 10, 8, 10, 11, // up
    12, 13, 14, 12, 14, 15, // left
    16, 17, 18, 16, 18, 19, // down
    20, 21, 22, 20, 22, 23 // back
  ]),
  normals: new Float32Array([
    0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v1-v2-v3 front
    1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // v0-v3-v4-v5 right
    0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v5-v6-v1 up
    -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // v1-v6-v7-v2 left
    0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, // v7-v4-v3-v2 down
    0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0 // v4-v7-v6-v5 back
  ]),
  colors: new Float32Array([ // Colors
    0.32, 0.18, 0.56, 0.32, 0.18, 0.56, 0.32, 0.18, 0.56, 0.32, 0.18, 0.56, // v0-v1-v2-v3 front
    0.5, 0.41, 0.69, 0.5, 0.41, 0.69, 0.5, 0.41, 0.69, 0.5, 0.41, 0.69, // v0-v3-v4-v5 right
    0.78, 0.69, 0.84, 0.78, 0.69, 0.84, 0.78, 0.69, 0.84, 0.78, 0.69, 0.84, // v0-v5-v6-v1 up
    0.0, 0.32, 0.61, 0.0, 0.32, 0.61, 0.0, 0.32, 0.61, 0.0, 0.32, 0.61, // v1-v6-v7-v2 left
    0.27, 0.58, 0.82, 0.27, 0.58, 0.82, 0.27, 0.58, 0.82, 0.27, 0.58, 0.82, // v7-v4-v3-v2 down
    0.73, 0.82, 0.93, 0.73, 0.82, 0.93, 0.73, 0.82, 0.93, 0.73, 0.82, 0.93, // v4-v7-v6-v5 back
  ])
};

// Indices of the vertices
const shadow_vertex_shader_source = `
    attribute vec4 a_Position;
    uniform mat4 u_MvpMatrix;

    void main(){
        gl_Position = u_MvpMatrix * a_Position;
    }
`;

const shadow_fragment_shader_source = `
    #ifdef GL_ES
    precision mediump float;
    #endif

    void main(){
        gl_FragColor = vec4(gl_FragCoord.z, 1.0, 0.0, 1.0);
    }
`;

const vertex_shader_source = `
    attribute vec4 a_Position;
    attribute vec4 a_Color;

    uniform mat4 u_MvpMatrix;
    uniform mat4 u_MvpMatrixFromLight;

    varying vec4 v_PositionFromLight;
    varying vec4 v_Color;
    void main(){
        gl_Position = u_MvpMatrix * a_Position;

        v_PositionFromLight = u_MvpMatrixFromLight * a_Position;
        v_Color = a_Color;
    }
`;

const fragment_shader_source = `
    #ifdef GL_ES
    precision mediump float;
    #endif

    uniform sampler2D u_ShadowMap;
    varying vec4 v_PositionFromLight;
    varying vec4 v_Color;
    void main(){
        vec3 shadowCoord =(v_PositionFromLight.xyz / v_PositionFromLight.w)/2.0 +0.5;
        vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);
        float depth = rgbaDepth.r;
        float visibility = (shadowCoord.z > depth + 0.005) ? 0.7 : 1.0;

        gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);
    }
`;